Aller au contenu principal

Opérations terminales

forEach

La méthode forEach est une opération terminale très couramment utilisée avec les flux Java (streams). Elle permet d'appliquer une action à chaque élément du flux. Voici une explication détaillée :

Fonctionnement de forEach

  • Itération sur les éléments :
    • forEach parcourt chaque élément du flux et exécute une action spécifiée sur chaque élément.
  • Action consommateur :
    • L'action est définie par un objet Consumer, qui est une interface fonctionnelle avec une seule méthode accept(T t). Cette méthode prend un élément du flux comme argument et effectue une action sur cet élément.
  • Opération terminale :
    • forEach est une opération terminale, ce qui signifie qu'elle consomme le flux. Une fois que forEach a été appelé, vous ne pouvez plus effectuer d'autres opérations sur ce flux.
  • Effets de bord :
    • forEach est principalement utilisé pour les effets de bord, tels que l'affichage d'éléments, l'écriture dans un fichier ou la modification d'une variable externe. Il n'est pas conçu pour renvoyer une nouvelle collection ou transformer les éléments du flux.
  • Ordre :
    • Lorsque le stream est séquentiel, forEach garanti de respecter l'ordre d'apparition des éléments.
    • Lorsque le stream est parallélisé avec parallelStream, il n'y a aucune garantie sur l'ordre dans lequel les éléments seront traités.

Exemples d'utilisation

  1. Affichage des éléments :

    List<String> mots = List.of("pomme", "banane", "orange");
    mots.stream().forEach(System.out::println);

    Cet exemple affiche chaque mot de la liste sur la console.

  2. Accès à une variable externe :

    List<String> noms = List.of("Alice", "Bob", "Clara");
    List<String> resultat = new ArrayList<>();

    noms.stream()
    .filter(n -> n.length() > 3)
    .forEach(n -> resultat.add(n)); // accès à la variable externe resultat

    System.out.println(resultat); // [Alice, Clara]

    resultat est accessible car la référence ne change pas — on ne réassigne pas resultat, on modifie seulement son contenu.

Points importants

  • forEach est utilisé pour effectuer des actions sur chaque élément, mais il ne renvoie pas de résultat.
  • Il est important de noter que les lambdas utilisées dans forEach ne doivent pas tenter de modifier la source du stream.
  • Pour transformer les éléments d'un flux et renvoyer une nouvelle collection, utilisez map et collect.
  • Pour filtrer les éléments d'un flux, utilisez filter.

Quand utiliser forEach

  • Lorsque vous devez effectuer une action sur chaque élément d'un flux sans renvoyer de résultat.
  • Pour les effets de bord, tels que l'affichage, l'écriture dans un fichier ou la modification de variables externes.

BinaryOperator

Un BinaryOperator<T> est une interface fonctionnelle qui prend deux arguments du même type et retourne un résultat du même type.

BinaryOperator<Integer> addition = (a, b) -> a + b;
System.out.println(addition.apply(3, 5)); // 8

BinaryOperator<String> concatenation = (a, b) -> a + " " + b;
System.out.println(concatenation.apply("Bonjour", "monde")); // Bonjour monde

C'est l'interface utilisée par reduce() pour combiner les éléments d'un stream un par un :

[1, 2, 3, 4, 5]
1 + 2 = 3
3 + 3 = 6
6 + 4 = 10
10 + 5 = 15

On peut aussi utiliser une référence de méthode existante :

BinaryOperator<Integer> max = Integer::max;
System.out.println(max.apply(10, 20)); // 20

Reduce

La méthode reduce en Java est une opération terminale puissante utilisée dans la programmation fonctionnelle avec les streams. Elle permet de combiner les éléments d'un flux en une seule valeur. Voici une explication détaillée :

Fonctionnement de reduce

La méthode reduce prend généralement deux arguments :

  • Une valeur d'identité (identity) : C'est la valeur initiale de l'accumulation, et c'est aussi le résultat par défaut si le flux est vide.
  • Une fonction d'accumulateur : C'est une fonction qui prend deux arguments : la valeur d'accumulation courante et un élément du flux. Elle renvoie une nouvelle valeur d'accumulation.

Voici la signature générale de la méthode reduce :

T reduce(T identity, BinaryOperator<T> accumulator)

Exemples d'utilisation

  1. Somme des éléments d'un flux :

    List<Integer> nombres = List.of(1, 2, 3, 4, 5);
    int somme = nombres.stream().reduce(0, (a, b) -> a + b);
    System.out.println(somme); // Output: 15

    Dans cet exemple :

    • 0 est la valeur d'identité.
    • (a, b) -> a + b est la fonction d'accumulateur qui additionne les éléments.
  2. Concaténation de chaînes de caractères :

    List<String> mots = List.of("Bonjour", "le", "monde");
    String phrase = mots.stream().reduce("", (a, b) -> a + " " + b);
    System.out.println(phrase); // Output: Bonjour le monde

    Ici :

    • "" est la chaîne vide initiale.
    • (a, b) -> a + " " + b concatène les chaînes avec un espace.
  3. Trouver le maximum :

    List<Integer> nombres = List.of(5, 2, 8, 1, 9);
    int max = nombres.stream().reduce(Integer.MIN_VALUE, Integer::max);
    System.out.println(max); // Output: 9

    Dans ce cas :

    • Integer.MIN_VALUE est la valeur d'identité.
    • Integer::max est une référence de méthode qui trouve le maximum entre deux nombres.

Points importants

  • reduce est une opération terminale, ce qui signifie qu'elle consomme le flux et produit un résultat.
  • Il est essentiel de choisir une valeur d'identité appropriée pour garantir un résultat correct.
  • La fonction d'accumulateur doit être associative, c'est-à-dire que l'ordre des opérations ne doit pas affecter le résultat.
  • lors de l'utilisation de reduce avec les flux parallèles, il est important que la fonction d'accumulation soit associative et sans effets secondaires.

Avantages de reduce

  • Il permet d'exprimer des opérations de réduction de manière concise et lisible.
  • Il s'intègre bien avec les autres opérations de flux, ce qui facilite la création de pipelines de traitement de données complexes.
  • Il permet d'écrire du code de type fonctionnel, et donc de bénéficier de tous les avantages de ce paradigme de programmation.

Collect

La méthode collect() est une opération terminale des Streams en Java qui sert à rassembler les éléments transformés d’un Stream dans une structure mutable comme une liste, un ensemble, une chaîne de caractères, une map, ou même une valeur agrégée personnalisée.

Elle utilise généralement la classe utilitaire Collectors pour spécifier le type de collecte.

1. Collecter dans une liste

List<String> nomsAvecA = noms.stream()
.filter(n -> n.startsWith("A"))
.collect(Collectors.toList());

2. Collecter dans un ensemble (Set)

Set<String> nomsUniques = noms.stream()
.map(String::toLowerCase)
.collect(Collectors.toSet());

3. Joindre des chaînes

String nomsConcat = noms.stream()
.collect(Collectors.joining(", "));

Résultat : "Alice, Bob, Alex, Charles"

4. Regrouper par clé (groupingBy)

groupingBy regroupe tous les éléments partageant la même clé dans une List. Si plusieurs éléments ont la même clé, ils se retrouvent tous dans la même liste.

List<String> noms = List.of("Alice", "Alex", "Bob", "Baptiste", "Clara", "Charles", "Camille");

Map<Character, List<String>> nomsParPremiereLettre = noms.stream()
.collect(Collectors.groupingBy(n -> n.charAt(0)));

System.out.println(nomsParPremiereLettre);
// {A=[Alice, Alex], B=[Bob, Baptiste], C=[Clara, Charles, Camille]}

Chaque clé ('A', 'B', 'C') pointe vers la liste de tous les noms commençant par cette lettre.

5. Associer clé-valeur (toMap)

List<String> noms = List.of("Alice", "Bob", "Charles");

Map<String, Integer> nomEtLongueur = noms.stream()
.collect(Collectors.toMap(n -> n, String::length));

System.out.println(nomEtLongueur);
// {Alice=5, Bob=3, Charles=7}

Le premier argument est la fonction qui produit la clé, le deuxième celle qui produit la valeur.

En résumé :

  • collect() est puissant et polyvalent.
  • Il convertit le Stream en une structure de données utilisable.
  • À utiliser chaque fois que tu veux récupérer un résultat à partir d'un Stream.

Count

En programmation fonctionnelle Java, count est une opération terminale utilisée avec les flux (streams) pour compter le nombre d'éléments dans le flux.

Fonctionnement de count

  • Comptage des éléments :
    • count parcourt tous les éléments du flux et renvoie le nombre total d'éléments.
  • Opération terminale :
    • Comme toutes les opérations terminales, count consomme le flux. Une fois que count a été appelé, vous ne pouvez plus effectuer d'autres opérations sur ce flux.
  • Retourne un long :
    • La méthode count renvoie une valeur de type long, ce qui permet de compter de très grands flux d'éléments.

Exemples d'utilisation

  1. Compter tous les éléments d'une liste :

    List<String> mots = List.of("pomme", "banane", "orange", "kiwi");
    long nombreDeMots = mots.stream().count();
    System.out.println("Nombre de mots : " + nombreDeMots); // Output: Nombre de mots : 4
  2. Compter les éléments qui remplissent une condition :

    List<Integer> nombres = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    long nombreDeNombresPairs = nombres.stream().filter(n -> n % 2 == 0).count();
    System.out.println("Nombre de nombres pairs : " + nombreDeNombresPairs); // Output: Nombre de nombres pairs : 5
  3. Compter les lignes d'un fichier :

    try {
    long nombreDeLignes = Files.lines(Paths.get("monfichier.txt")).count();
    System.out.println("Nombre de lignes : " + nombreDeLignes);
    } catch (IOException e) {
    e.printStackTrace();
    }

Points importants

  • count est une opération simple et efficace pour obtenir le nombre d'éléments dans un flux.
  • Il est souvent utilisé en combinaison avec d'autres opérations de flux, telles que filter, pour compter les éléments qui répondent à des critères spécifiques.
  • count est une opération terminale, ce qui signifie qu'elle termine le flux.

Quand utiliser count

  • Lorsque vous avez besoin de connaître le nombre total d'éléments dans un flux.
  • Lorsque vous devez compter les éléments qui remplissent une condition spécifique.

anyMatch, allMatch, noneMatch

Ces trois opérations terminales vérifient si les éléments d'un stream respectent une condition. Elles retournent toutes un boolean.

MéthodeRetourne true si...
anyMatchau moins un élément respecte la condition
allMatchtous les éléments respectent la condition
noneMatchaucun élément ne respecte la condition
List<Integer> nombres = List.of(1, 2, 3, 4, 5);

boolean auMoinsUnPair = nombres.stream().anyMatch(n -> n % 2 == 0); // true
boolean tousPairs = nombres.stream().allMatch(n -> n % 2 == 0); // false
boolean aucunNegatif = nombres.stream().noneMatch(n -> n < 0); // true

Elles s'arrêtent dès que le résultat est déterminé — pas besoin de parcourir tout le stream :

List<String> noms = List.of("Alice", "Bob", "Clara");

boolean existeAvecA = noms.stream().anyMatch(n -> n.startsWith("A")); // true — s'arrête à "Alice"

findFirst

findFirst retourne le premier élément du stream sous forme d'Optional, car le stream pourrait être vide.

List<String> noms = List.of("Alice", "Bob", "Clara");

Optional<String> premier = noms.stream()
.filter(n -> n.startsWith("C"))
.findFirst();

System.out.println(premier.orElse("aucun")); // Clara
Optional<String> absent = noms.stream()
.filter(n -> n.startsWith("Z"))
.findFirst();

System.out.println(absent.orElse("aucun")); // aucun

Règle pratique : utilisez anyMatch pour vérifier une existence, findFirst pour récupérer l'élément lui-même.


Documentation Oracle : forEach, reduce, collect, count, anyMatch, allMatch, noneMatch, findFirst (java.util.stream.Stream) · toList, toSet, toMap, joining, groupingBy (java.util.stream.Collectors)